package pzl.media.dsp;

import android.media.MediaCodec;
import android.media.MediaFormat;
import android.os.Bundle;
import android.os.Environment;
import android.util.Log;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.widget.Toast;

import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.util.Arrays;

/**
 * https://www.jianshu.com/p/f5a1c9318524
 */
public class H264DecoderActivity extends AppCompatActivity implements SurfaceHolder.Callback {
    final static String TAG = "jinx_MediaCodec";
    SurfaceView surfaceView;
    SurfaceHolder surfaceHolder;

    private Thread mDecodeThread;
    private MediaCodec mMediaCodec;
    private boolean mStopFlag = false;

    //本地h264文件
    private String FileName = "pzl.h264";
    private String filePath = Environment.getExternalStorageDirectory() + "/" + FileName;
    private DataInputStream mLocalDataInputStream;

    private static final int VIDEO_WIDTH = 1920;
    private static final int VIDEO_HEIGHT = 1088;
    //帧率
    private int FrameRate = 15;
    private Boolean UseSPSandPPS = false;


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_h264_decoder);

        loadH264File();

        surfaceView = findViewById(R.id.MediaCodecSurf2);
        surfaceHolder = surfaceView.getHolder();
        surfaceHolder.addCallback(this);

    }

    /**
     * 查找 H264视频压缩文件
     */
    private void loadH264File() {
        File f = new File(filePath);
        if (null == f || !f.exists() || f.length() == 0) {
            Toast.makeText(this, "文件不存在", Toast.LENGTH_LONG).show();
            return;
        }

        try {
            mLocalDataInputStream = new DataInputStream(new FileInputStream(new File(filePath)));
        } catch (FileNotFoundException e) {
            e.printStackTrace();
            try {
                mLocalDataInputStream.close();
            } catch (IOException e1) {
                e1.printStackTrace();
            }
        }
    }


    @Override
    public void surfaceCreated(@NonNull SurfaceHolder holder) {
        try {
            //创建解码器 h264
            mMediaCodec = MediaCodec.createDecoderByType("video/avc");
        } catch (IOException e) {
            e.printStackTrace();
        }
        //初始化 封装描述媒体数据格式的信息(包括音频或视频)，以及可选的特性元数据。
        final MediaFormat mediaformat = MediaFormat.createVideoFormat("video/avc", VIDEO_WIDTH, VIDEO_HEIGHT);
        //  h264中的pps及sps数据
        if (UseSPSandPPS) {
            byte[] header_sps = {0, 0, 0, 1, 103, 66, 0, 42, (byte) 149, (byte) 168, 30, 0, (byte) 137, (byte) 249, 102, (byte) 224, 32, 32, 32, 64};
            byte[] header_pps = {0, 0, 0, 1, 104, (byte) 206, 60, (byte) 128, 0, 0, 0, 1, 6, (byte) 229, 1, (byte) 151, (byte) 128};
            mediaformat.setByteBuffer("csd-0", ByteBuffer.wrap(header_sps));
            mediaformat.setByteBuffer("csd-1", ByteBuffer.wrap(header_pps));
        }
        //设置帧率
        mediaformat.setInteger(MediaFormat.KEY_FRAME_RATE, FrameRate);
        // format	如果为解码器，此处表示输入数据的格式；如果为编码器，此处表示输出数据的格式。
        // surface	指定Surface，用于解码器输出的渲染。如果编解码器不生成原始视频输出(例如，不是视频解码器)和/或想配置解码器输出ByteBuffer，则传null。）
        // crypto	如果需要给媒体数据加密，此处指定一个crypto类.
        // flags	如果正在配置的对象是用作编码器，此处加上CONFIGURE_FLAG_ENCODE = 1 标签。
        mMediaCodec.configure(mediaformat, holder.getSurface(), null, 0);
        startDecodingThread();
    }

    @Override
    public void surfaceChanged(@NonNull SurfaceHolder holder, int format, int width, int height) {
    }

    @Override
    public void surfaceDestroyed(@NonNull SurfaceHolder holder) {
        mMediaCodec.stop();
        mMediaCodec.release();
    }

    private void startDecodingThread() {
        mMediaCodec.start();
        mDecodeThread = new Thread(new DecodeRunnable());
        mDecodeThread.start();
    }


    private class DecodeRunnable implements Runnable {
        @Override
        public void run() {
            try {
                decodeWork();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

        private void decodeWork() {
            ByteBuffer[] inputBuffers = mMediaCodec.getInputBuffers();
            //解码后数据的相关信息
            MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
            long startMs = System.currentTimeMillis();
            //从mediaCodec取可用的byteBuffer的超时时间
            long timeout = 10000;
            //帧分隔符 00 00 00 01 或 00 00 01
            byte[] marker = new byte[]{0, 0, 0, 1};
            //本地拿到的数据不存在时的替代帧
            byte[] emptyFrame = new byte[]{0x00, 0x00, 0x01, 0x20};
            //本地H264文件的byte数据
            byte[] h264LocalBytes = null;
            try {
                h264LocalBytes = getBytes(mLocalDataInputStream);
            } catch (IOException e) {
                e.printStackTrace();
            }
            int localH264BytesLen;
            while (mStopFlag == false) {
                localH264BytesLen = h264LocalBytes.length;
                if (localH264BytesLen == 0) {
                    h264LocalBytes = emptyFrame;
                }
                //记录帧开始的位置
                int startIndex = 0;
                final int h264FileBytesLength = localH264BytesLen;
                while (true) {
                    if (h264FileBytesLength == 0 || startIndex >= h264FileBytesLength) {
                        break;
                    }
                    //计算下一帧开始的位置
                    int nextFrameStart = KMPMatch(marker, h264LocalBytes, startIndex + 2, h264FileBytesLength);
                    Log.i(TAG, " startIndex = " + startIndex + " , nextFrameStart = " + nextFrameStart);
                    if (nextFrameStart == -1) {
                        nextFrameStart = h264FileBytesLength;
                    } else {
                    }
                    //向MediaCodec申请可用的InputBuffer资源（存放待解码的H264数据）
                    int inputBufferIndex = mMediaCodec.dequeueInputBuffer(timeout);
                    if (inputBufferIndex >= 0) {//index>0， 申请到了资源，资源位置为inputBufferIndex
                        ByteBuffer byteBuffer = inputBuffers[inputBufferIndex];//申请到的byteBuffer资源
                        byteBuffer.clear();//以防别人留下的脏数据万一，清理下
                        byteBuffer.put(h264LocalBytes, startIndex, nextFrameStart - startIndex);//把我要解码的数据放进去（一帧）
                        //提交数据，在给指定Index的inputbuffer[]填充数据后，调用这个函数把数据传给解码器
                        mMediaCodec.queueInputBuffer(inputBufferIndex, 0, nextFrameStart - startIndex, 0, 0);
                        startIndex = nextFrameStart;
                    } else {
                        continue;
                    }
                    //尝试获取DSP解码后的数据（timeout）
                    int outputBufferIndex = mMediaCodec.dequeueOutputBuffer(info, timeout);
                    if (outputBufferIndex >= 0) {//解码完成，解码后的数据在outputBufferIndex位置里
                        while (info.presentationTimeUs / 1000 > System.currentTimeMillis() - startMs) {
                            try {
                                Thread.sleep(100);//人为控制帧速度
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                        }
                        boolean doRender = (info.size != 0);
                        //使用此方法将输出buffer返回给codec或将其渲染在输出surface。
                        //boolean render：如果在配置codec时指定了一个有效的surface，则传递true会将此输出buffer在surface上渲染。一旦不再使用buffer，该surface将把buffer释放回codec。
                        mMediaCodec.releaseOutputBuffer(outputBufferIndex, doRender);
                    } else {
                    }
                }
                mStopFlag = true;
            }
        }
    }

    /**
     * 本地H264文件转字节数组
     */
    public static byte[] getBytes(InputStream is) throws IOException {
        int len;
        int size = 1024;
        byte[] buf;
        if (is instanceof ByteArrayInputStream) {
            size = is.available();
            buf = new byte[size];
            len = is.read(buf, 0, size);
        } else {
            ByteArrayOutputStream bos = new ByteArrayOutputStream();
            buf = new byte[size];
            while ((len = is.read(buf, 0, size)) != -1)
                bos.write(buf, 0, len);
            buf = bos.toByteArray();
        }
        return buf;
    }

    /**
     * 计算下一帧开始的位置
     *
     * @param frameMarker
     * @param localH264Bytes
     * @param start
     * @param h264FileBytesLength
     * @return
     */
    int KMPMatch(byte[] frameMarker, byte[] localH264Bytes, int start, int h264FileBytesLength) {
        try {
            Thread.sleep(30);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        int[] lsp = computeLspTable(frameMarker);

        int j = 0;  // Number of chars matched in pattern
        for (int i = start; i < h264FileBytesLength; i++) {
            while (j > 0 && localH264Bytes[i] != frameMarker[j]) {
                // Fall back in the pattern
                j = lsp[j - 1];  // Strictly decreasing
            }
            if (localH264Bytes[i] == frameMarker[j]) {
                // Next char matched, increment position
                j++;
                if (j == frameMarker.length)
                    return i - (j - 1);
            }
        }
        return -1;  // Not found
    }

    int[] computeLspTable(byte[] frameMarker) {
        Log.i(TAG, "computeLspTable param frameMarker = " + Arrays.toString(frameMarker));
        int[] lsp = new int[frameMarker.length];
        lsp[0] = 0;  // Base case
        for (int i = 1; i < frameMarker.length; i++) {
            // Start by assuming we're extending the previous LSP
            int j = lsp[i - 1];
            while (j > 0 && frameMarker[i] != frameMarker[j])
                j = lsp[j - 1];
            if (frameMarker[i] == frameMarker[j])
                j++;
            lsp[i] = j;
        }
        Log.i(TAG, "computeLspTable return lsp = " + Arrays.toString(lsp));
        return lsp;
    }

}
